import React, {Component} from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
// import py1_1 from '../static/py1_1.md'
// import marked from 'marked';

class Py21 extends Component {
    constructor(props) {
        super(props);
        this.state = {
           value: `

到目前为止，我们还没有处理过那些样式不规范的数据，要么是使用样式规范的数据源，
要么就是彻底放弃样式不符合我们预期的数据。但是在网络数据采集中，你通常无法对采
集的数据样式太挑剔。

由于错误的标点符号、大小写字母不一致、断行和拼写错误等问题，零乱的数据（dirty
data）是网络中的大问题。本章将介绍一些工具和技术，通过改变代码的编写方式，帮你
从源头控制数据零乱的问题，并且对已经进入数据库的数据进行清洗。

> 7.1　编写代码清洗数据

和写代码处理异常一样，你也应该学习编写预防型代码来处理意外情况。

在语言学里有一个模型叫 n-gram，表示文字或语言中的 n 个连续的单词组成的序列。在进
行自然语言分析时，使用 n-gram 或者寻找常用词组，可以很容易地把一句话分解成若干个
文字片段。

这一节我们将重点介绍如何获取格式合理的 n-gram，并不用它们
做任何分析。在第 8 章，
我们再用 2-gram 和 3-gram 来做文本摘要提取和语法分析。
下面的代码将返回维基百科词条“Python programming language”的 2-gram 列表：
   
    from urllib.request import urlopen
    from bs4 import BeautifulSoup

    def ngrams(input, n):
        input = input.split(' ')
        output = []
        for i in range(len(input)-n+1):
            output.append(input[i:i+n])
        return output

    html = urlopen("http://en.wikipedia.org/wiki/Python_(programming_language)")
    bsObj = BeautifulSoup(html)
    content = bsObj.find("div", {"id":"mw-content-text"}).get_text()
    ngrams = ngrams(content, 2)
    print(ngrams)
    print("2-grams count is: "+str(len(ngrams)))
 
ngrams 函数把一个待处理的字符串分成单词序列（假设所有单词按照空格分开），然后增
加到 n-gram 模型（本例中是 2-gram）里形成以每个单词开始的二元数组。

这段程序会从文字中返回一些有意思同时也很有用的 2-gram 序列：
    
    ['of', 'free'], ['free', 'and'], ['and', 'open-source'], ['open-source', 'software']
  
不过，同时也会出现一些零乱的数据：
   
    ['software\\nOutline\\nSPDX\\n\\n\\n\\n\\n\\n\\n\\n\\nOperating', 'system\\nfamilies\\n\\n\\n\\n
    AROS\\nBSD\\nDarwin\\neCos\\nFreeDOS\\nGNU\\nHaiku\\nInferno\\nLinux\\nMach\\nMINIX\\nOpenS
    olaris\\nPlan'], ['system\\n\\families\\n\\n\\n\\nAROS\\nBSD\\nDarwin\\neCos\\nFreeDOS\\nGNU\\nHaiku\\n
    Inferno\\nLinux\\nMach\\nMINIX\\nOpenSolaris\\nPlan', '9\\nReactOS\\nTUD:OS\\n\\n
    \\n\\n\\n\\n\\n\\n\\nDevelopment\\n\\n\\n\\nBasic'], ['9\\nReactOS\\nTUD:OS\\n\\n\\n\\n\\n\\n\\n\\n\\n
    Development\\n\\n\\n\\nBasic', 'For']
    
另外，因为每个单词（除了最后一个单词）都要创建一个 2-gram 序列，所以这个词条里共
有 7411 个 2-gram 序列。这并不是一个非常便于管理的数据集！

让我们首先用一些正则表达式来移除转义字符（ \\n ），再把 Unicode 字符过滤掉。我们可以
通过下面的函数对之前输出的结果进行清理：
     
    def ngrams(input, n):
        content = re.sub('\\n+', " ", content)
        content = re.sub(' +', " ", content)
        content = bytes(content, "UTF-8")
        content = content.decode("ascii", "ignore")
        print(content)
        input = input.split(' ')
        output = []
        for i in range(len(input)-n+1):
            output.append(input[i:i+n])
        return output
     
这里首先把内容中的换行符（或者多个换行符）替换成空格，然后把连续的多个空格替换
成一个空格，确保所有单词之间只有一个空格。最后，把内容转换成 UTF-8 格式以消除转
义字符。

这几步已经可以大大改善输出结果了，但是还有一些问题：
     
    ['Pythoneers.[43][44]', 'Syntax'], ['7', '/'], ['/', '3'], ['3', '=='], ['==', '
    2']
    
因此，需要再增加一些规则来处理数据。我们还可以制定一些规则让数据变得更规范：
* 剔除单字符的“单词”，除非这个字符是“i”或“a”；
* 剔除维基百科的引用标记（方括号包裹的数字，如 [1]）；
* 剔除标点符号（注意：这个规则有点儿矫枉过正，在第 9 章我们将详细介绍，本例暂时
这样处理）。

现在“清洗任务”列表变得越来越长，让我们把规则都移出来，单独建一个函数，就叫
cleanInput ：
   
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re
    import string

    def cleanInput(input):
        input = re.sub('\n+', " ", input)
        input = re.sub('\[[0-9]*\]', "", input)
        input = re.sub(' +', " ", input)
        input = bytes(input, "UTF-8")
        input = input.decode("ascii", "ignore")
        cleanInput = []
        input = input.split(' ')
        for item in input:
            item = item.strip(string.punctuation)
            if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
                cleanInput.append(item)
        return cleanInput

    def ngrams(input, n):
        input = cleanInput(input)
        output = []
        for i in range(len(input)-n+1):
            output.append(input[i:i+n])
        return output
   
这里用 import string 和 string.punctuation 来获取 Python 所有的标点符号。你可以在
Python 命令行看看标点符号有哪些：
    
    >>> import string
    >>> print(string.punctuation)
    !"#$%&'()*+,-./:;<=>?@[\]^_'{|}~
   
在循环体中用 item.strip(string.punctuation) 对内容中的所有单词进行清洗，单词两端
的任何标点符号都会被去掉，但带连字符的单词（连字符在单词内部）仍然会保留。
这样输出的 2-gram 结果就更干净了：
  
    ['Linux', 'Foundation'], ['Foundation', 'Mozilla'], ['Mozilla', 'Foundation'], [
    'Foundation', 'Open'], ['Open', 'Knowledge'], ['Knowledge', 'Foundation'], ['Fou
    ndation', 'Open'], ['Open', 'Source']

> 数据标准化

每个人都会遇到一些样式设计不够人性化的网页，比如“请输入你的电话号码。号码格式
必须是 xxx-xxx-xxxx”。

作为一名优秀的程序员，你可能会问：“为什么不自动地对输入的信息进行清洗，去掉非
数字内容，然后自动把数据加上分隔符呢？”数据标准化过程要确保清洗后的数据在语言
学或逻辑上是等价的，比如电话号码虽然显示成“(555) 123-4567”和“555.123.4567”两
种形式，但是实际号码是一样的。

还用之前的 n-gram 示例，让我们在上面增加一些数据标准化特征。

这段代码有一个明显的问题，就是输出结果中包含太多重复的 2-gram 序列。程序把每个
2-gram 序列都加入了列表，没有统计过序列的频率。掌握 2-gram 序列的频率，而不只是
知道某个序列是否存在，这不仅很有意思，而且有助于对比不同的数据清洗和数据标准化
算法的效果。如果数据标准化成功了，那么唯一的 n-gram 序列数量就会减少，而 n-gram
序列的总数（任何一个 n-gram 序列和与之重复的序列被看成一个 n-gram 序列）不变。也
就是说，同样数量的 n-gram 序列，经过去重之后“容量”（bucket）会减少。

不过 Python 的字典是无序的，不能像数组一样直接对 n-gram 序列频率进行排序。字典内
部元素的位置不是固定的，排序之后再次使用时还是会变化，除非你把排序过的字典里的
值复制到其他类型中进行排序。在 Python 的 collections 库里面有一个 OrderedDict 可以
解决这个问题：
     
    from collections import OrderedDict

    ...

    ngrams = ngrams(content, 2)
    ngrams = OrderedDict(sorted(ngrams.items(), key=lambda t: t[1], reverse=True))
    print(ngrams)
    
这里我用了 Python 的排序函数（\https://docs.python.org/3/howto/sorting.html）把序列频率转
换成 OrderedDict 对象，并按照频率值排序。结果如下：
    
    ("['Software', 'Foundation']", 40), ("['Python', 'Software']", 38), ("['of', 'th
    e']", 35), ("['Foundation', 'Retrieved']", 34), ("['of', 'Python']", 28), ("['in
    ', 'the']", 21), ("['van', 'Rossum']", 18)
    
，词条内容一个有 7696 个 2-gram 序列，其中不重复的 2-gram 序列有
6005 个，频率最高的 2-gram 序列是“Software Foundation”和“Python Software”。但是，
仔细观察结果发现还有字母大小写的影响，“Python Software”有两次是“Python software”
的形式。同样，“van Rossum”和“Van Rossum”也是作为两个序列统计的。

因此，增加一行代码：
    
    input = input.upper()

到 cleanInput 函数里，这样 2-gram 序列的总数还是 7696，而不重复的 2-gram 序列减少到
了 5882 个。

除了这些，还需要再考虑一下，自己计划为数据标准化的进一步深入再投入多少计算能
力。很多单词在不同的环境里会使用不同的拼写形式，其实都是等价的，但是为了解决这
种等价关系，你需要对每个单词进行检查，判断是否和其他单词有等价关系。

比如，“Python 1st”和“Python first”都出现在 2-gram 序列列表里。但是，如果增加一条
规则：“让所有‘first’‘second’‘third’……与 1st，2nd，3rd……等价”，那么每个单词
都要额外增加十几次检查。
同理，连字符使用不一致（像“co-ordinated”和“coordinated”）、单词拼写错误以及其他
语病（incongruities），都可能对 n-gram 序列的分组结果造成影响，如果语病很严重的话，
还可能彻底打乱输出结果。

对带连字符单词的一个处理方法是，先把连字符去掉，然后把单词当作一个字符串，这可
能需要在程序中增加一步操作。但是，这样做也可能把带连字符的短语（这是很常见的，
比如“just-in-time”“object-oriented”等）处理成一个字符串。要是换一种做法，把连字符
替换成空格可能更好一点儿。但是就得准备见到“co ordinated”和“ordinated attack”之类
的 2-gram 序列了！
           
           
    
           
           `,
        };
      }
    render() {
        return (
            <div >
                {/* 111111111111111
                <Py112 /> */}
                <MarkdownPreview value={this.state.value}/>
            </div>
        )
    }
}

export default Py21